import numpy
from copy import deepcopy
from .ancillaryvariables import AncillaryVariables
from .cellmethods        import CellMethods
from .comparison         import Comparison
from .coordinate         import Coordinate
from .data               import Data
from .flags              import Flags
from .utils              import CHUNKSIZE, parse_indices
from .variable           import Variable, SubspaceVariable

# ====================================================================
#
# Field object
#
# ====================================================================

class Field(Variable):
    '''

A field construct according to the CF data model.

A field is a container for a data array and metadata comprising
properties to describe the physical nature of the data and a
coordinate system (called a space) which describes the positions of
each element of the data array.

It is structured in exactly the same way as a field construct defined
by the CF data model.

The field's space may contain coordinates and cell measures (which
themselves contain data arrays and properties to describe them) and
transforms to describe how other auxiliary coordinates may be
computed.

As in the CF data model, all components of a field are optional.

'''

    _special_attributes = Variable._special_attributes.union(set(('ancillary_variables',
                                                                  'cell_methods',
                                                                  'flag_values',
                                                                  'flag_masks',
                                                                  'flag_meanings',
                                                                  ))
                                                             )

    def _binary_operation(self, other, method):
        '''

Implement binary arithmetic and comparison operations on the data
array with metadata-aware broadcasting.

It is intended to be called by the binary arithmetic and comparison
methods, such as __sub__(), __imul__() and __lt__().

:Parameters:

    operation : str
        The binary arithmetic or comparison method name (such as
        '__rdiv__' or '__ge__').

:Returns:

    out : Field
        The new Field, or the same Field if the operation was
        in-place.

**Examples**

>>> h = f._binary_operation(g, '__add__')
>>> h = f._binary_operation(g, '__ge__')
>>> f._binary_operation(g, '__isub__')

>>> g = f._binary_operation(2, '__add__')
>>> g = f._binary_operation(2, '__ge__')
>>> f._binary_operation(2, '__isub__')

'''
        if (isinstance(other, (float, int, long, bool, basestring)) or
            other is self):
            # ========================================================
            # Combine the field with one of the following:
            #
            #   * A python scalar
            #   * Itself
            #
            # These cases are special because they don't involve any
            # changes to the field's space and so can use the
            # metadata-UNaware Variable._binary_operation method.
            # ========================================================
            return super(Field, self)._binary_operation(other, method)
        #--- End: if

        if isinstance(other, Data) and other.size == 1:
            # ========================================================
            # Combine the field with a size 1 Data object
            #
            # This case is special because it doesn't involve any
            # changes to the field's space and so can use the
            # metadata-UNaware Variable._binary_operation method.
            # ========================================================
            other = other.copy()
            other.squeeze()
            return super(Field, self)._binary_operation(other, method)
        #--- End: if

        if isinstance(other, Comparison):
            # ========================================================
            # Combine the field with a Comparison object
            # ========================================================
            return NotImplemented
        #--- End: if

        if not isinstance(other, self.__class__):
            raise ValueError(
                "Can't combine '%s' with '%s'" %
                (self.__class__.__name__, other.__class__.__name__))
        #--- End: if

        # ============================================================
        # Still here? Then combine the field with another field
        # ============================================================

        # ------------------------------------------------------------
        # Analyse each space
        # ------------------------------------------------------------
        s = self.space.analyse()
        v = other.space.analyse()

        if s['warnings'] or v['warnings']:
            raise ValueError("Can't combine fields: %s" % 
                             (s['warnings'] or v['warnings']))

        # Check that at most one field has undefined dimensions
        if s['undefined_dims'] and v['undefined_dims']:
            raise ValueError(
"Can't combine fields: Both fields have undefined dimensions")

        # Find the dimension names which are present in both fields
        matching_ids = set(s['id_to_dim']).intersection(v['id_to_dim'])
        
        # Check that any matching dimensions defined by an auxiliary
        # coordinate are done so in both fields.
        for identity in set(s['id_to_aux']).symmetric_difference(v['id_to_aux']):
            if identity in matching_ids:
                raise ValueError(
"Can't combine fields: '%s' dimension defined by auxiliary in only 1 field" %
standard_name)
        #--- End: for

        # ------------------------------------------------------------
        # For matching dimension coordinates check that one of the
        # following is true:
        #
        # 1) They have equal size > 1 and their data arrays are
        # equivalent
        #
        # 2) They have unequal sizes and one of them has size 1
        #
        # 3) They have equal size = 1. In this case, if the data
        # arrays are not equivalent then the dimension will be omitted
        # from the result field's space.
        # ------------------------------------------------------------

        # List of size 1 dimensions to be completely removed from the
        # result field. Such a dimension's size 1 defining coordinates
        # have unequivalent data arrays.
        #
        # For example:
        # >>> remove_size1_dims
        # ['dim2']
        remove_size1_dims = []

        # List of matching dimensions with equivalent defining
        # dimension coordinate data arrays. 
        #
        # Note that we don't need to include matching dimensions with
        # equivalent defining *auxiliary* coordinate data arrays.
        #
        # For example:
        # >>> equivalent_data_matching_dimensions
        # [('dim2', 'dim0')]
        matching_dimensions_with_equivalent_data = []

        # For each field, list those of its matching dimensions which
        # need to be broadcast against the other field. I.e. those
        # dimensions which are size 1 but size > 1 in the other field.
        #
        # For example:
        # >>> s['broadcast_dims']
        # ['dim1']
        s['broadcast_dims'] = []
        v['broadcast_dims'] = []

        for identity in matching_ids:
            coord0 = s['id_to_coord'][identity]
            coord1 = v['id_to_coord'][identity]

            size0 = coord0.size
            size1 = coord1.size
            if size0 != size1:
                # Defining coordinates have different sizes
                if size0 == 1:
                    s['broadcast_dims'].append(s['id_to_dim'][identity])
                elif size1 == 1:
                    v['broadcast_dims'].append(v['id_to_dim'][identity])
                else:
                    raise ValueError(
"Can't combine fields: Can't broadcast '%s' dimensions with sizes %d and %d" %
(identity, size0, size1))

                continue
            #--- End: if

            # Still here? Then the defining coordinates have equal
            # size, so check that their data arrays are compatible.
            dim1_to_dim0 = {v['id_to_dim'][identity]: s['id_to_dim'][identity]}
            
            if coord0._equivalent_data(coord1, transpose=dim1_to_dim0):
                # We have equal sized coordinates with equivalent data
                # arrays ...
                dim0 = s['id_to_dim'][identity]
                dim1 = v['id_to_dim'][identity]
                if identity not in s['id_to_aux']:
                    # ... and they are dimension coordinates
                    matching_dimensions_with_equivalent_data.append((dim0, dim1))

            else:
                if coord0.size > 1:
                    # We have equal sized, size > 1 coordinates with
                    # unequivalent data arrays
                    raise ValueError(
"Can't combine fields: Incompatible '%s' coordinate data arrays" % identity)
                
                # We have equal sized, size 1 coordinates with
                # unequivalent data arrays, so flag this dimension to
                # be omitted from the result field.
                dim0 = s['id_to_dim'][identity]
                remove_size1_dims.append(dim0)
        #--- End: for

        # --------------------------------------------------------
        # Still here? Then the two fields are combinable!
        # --------------------------------------------------------

        # ------------------------------------------------------------
        # 2.1 Create copies of the two fields, unless it is an in
        # place combination, in which case we don't want to copy self)
        # ------------------------------------------------------------
        field1 = other.copy()

        inplace = method[2] == 'i'
        if not inplace:
            field0 = self.copy()
        else:
            field0 = self

        # Aliases for the field's space and data array
        space0 = field0.space
        space1 = field1.space
        data0  = field0.Data
        data1  = field1.Data

        # ------------------------------------------------------------
        # 2.2 Map the dimension names of space1 to the dimension names
        # of space0.
        #
        # Where this is not possible (because space1 has a dimension
        # which space0 hasn't) then a new space0 dimension name is
        # created.dim1_to
        # ------------------------------------------------------------
 
        # Map dimensions in field1 to dimensions in field0
        #
        # For example:
        # >>> dim1_to_dim0
        # {'dim1': 'dim0', 'dim2': 'dim1', 'dim0': 'dim2'}
        dim1_to_dim0 = {}

        s['new_dims'] = []
            
        for dim1 in space1.dimension_sizes:
            if dim1 in v['dim_to_id']:
                identity = v['dim_to_id'][dim1]
                if identity in matching_ids:
                    dim1_to_dim0[dim1] = s['id_to_dim'][identity]
                    continue
            #--- End: if

            # Still here? Then dim1 has no match in space0, so create
            # a new space0 dimension name.
            dim0 = space0.new_dimension()
            dim1_to_dim0[dim1] = dim0

            # Include this new dimension as a broadcast dimension,
            # regardless of the size of the dimension in space1. This
            # is a sneaky way of, a bit later, getting the coordinates
            # from space1 copied over to space0.
            s['new_dims'].append(dim0)
        #--- End: for
            
        # Map dimensions in field0 to dimensions in field1
        #
        # For example:
        # >>> dim0_to_dim1
        # {'dim0': 'dim1', 'dim1': 'dim2', 'dim2': 'dim0'}
        dim0_to_dim1 = dict((v, k) for k, v in dim1_to_dim0.iteritems())

        # ------------------------------------------------------------
        # Permute the dimensions of data0 so that:
        #
        # * All of the matching dimensions are the inner (fastest
        #   varying) dimensions
        # * All of the undefined dimensions are the outer (slowest
        #   varying) dimensions        
        # * All of the defined but unmatched dimensions are in the
        #   middle
        # ------------------------------------------------------------
        axes_unD = []  # Undefined axes
        axes_unM = []  # Defined but unmatched axes
        axes_M   = []  # Defined and matched axes
        for i, dim0 in enumerate(data0.order):
            if dim0 in s['undefined_dims']:
                axes_unD.append(i)
            elif s['dim_to_id'][dim0] in matching_ids:
                axes_M.append(i)
            else:
                axes_unM.append(i)
        #--- End: for
        axes                = axes_unD + axes_unM + axes_M
        start_of_unmatched0 = len(axes_unD)
        start_of_matched0   = start_of_unmatched0 + len(axes_unM)
        unmatched_indices0  = slice(start_of_unmatched0, start_of_matched0)
        field0.transpose(axes)

        # ------------------------------------------------------------
        # Permute the dimensions of data1 so that:
        #
        # * All of the matching dimensions are the inner (fastest
        #   varying) dimensions and in corresponding positions to
        #   data0
        # * All of the undefined dimensions are the outer (slowest
        #   varying) dimensions
        # * All of the defined but unmatched dimensions are in the
        #   middle
        # ------------------------------------------------------------
        axes_unD = []
        axes_unM = []
        axes_M   = []
        for i, dim1 in enumerate(data1.order):
            if dim1 in v['undefined_dims']:
                axes_unD.append(i)
            elif v['dim_to_id'][dim1] in matching_ids:
                continue
            else:
                axes_unM.append(i)
        #--- End: for
        for dim0 in data0.order[start_of_matched0:]:
            axes_M.append(data1.order.index(dim0_to_dim1[dim0]))
        #--- End: for
        axes                = axes_unD + axes_unM + axes_M
        start_of_unmatched1 = len(axes_unD)
        start_of_matched1   = start_of_unmatched1 + len(axes_unM)
        undefined_indices1  = slice(None, start_of_unmatched1)
        unmatched_indices1  = slice(start_of_unmatched1, start_of_matched1)
        matched_indices1    = slice(start_of_matched1, None)
        field1.transpose(axes)

        # ----------------------------------------------------------------
        # Make sure that matching dimensions have the same directions
        # ----------------------------------------------------------------
        for identity in matching_ids:
            dim1 = v['id_to_dim'][identity]
            if space1.dimension_sizes[dim1] > 1:
                dim0 = s['id_to_dim'][identity]
                if field1.Data.direction[dim1] != field0.Data.direction[dim0]:
                    field1.flip(dim1)
        #--- End: for

        # ------------------------------------------------------------
        # 2f. Insert size 1 dimensions into data0 to correspond to
        # defined but unmatched dimensions in data1
        #
        # For example, if   data0 is          T Y X
        #              and  data1 is        P Z Y X
        #              then data0 becomes P Z T y X
        # ------------------------------------------------------------
        original_start_of_unmatched0 = start_of_unmatched0
        for dim1 in data1.order[unmatched_indices1]:
            data0.expand_dims(start_of_unmatched0,
                              dim1_to_dim0[dim1],
                              data1.direction[dim1])
            start_of_unmatched0 += 1
            start_of_matched0   += 1
        #--- End: for

        # ------------------------------------------------------------
        # Insert size 1 dimensions into data1 to correspond to defined
        # but unmatched dimensions in data0
        #
        # For example, if   data0 is      P Z T Y X
        #              and  data1 is        P Z Y X
        #              then data1 becomes P Z T Y X
        # ------------------------------------------------------------
        for dim0 in data0.order[start_of_unmatched0:start_of_matched0]:
            data1.expand_dims(start_of_unmatched1,
                              start_of_unmatched1,  # Arbitrary, but unique, name
                              data0.direction[dim0])
            start_of_unmatched1 += 1
        #--- End: for

        # ------------------------------------------------------------
        # Insert size 1 dimensions into data0 to correspond to
        # undefined dimensions (of any size) in data1.
        #
        # For example, if   data0 is          P Z T Y X
        #              and  data1 is      4 1 P Z T Y X
        #              then data0 becomes 1 1 P Z T Y X
        # ------------------------------------------------------------
        start_of_unmatched0 = original_start_of_unmatched0
        for dim1 in data1.order[undefined_indices1]:
            dim0 = dim1_to_dim0[dim1]
            data0.expand_dims(start_of_unmatched0,
                              dim0,
                              data1.direction[dim1])
            start_of_unmatched0 += 1
        #--- End: for

        # ============================================================
        # 3. Combine the data objects
        #
        # By now, data0.ndim >= data1.ndim and all of the dimension
        # names in data0 are correct for space0. Therefore, the
        # dimension names of the combined data will all be ok.
        # ============================================================
        data0                     = getattr(data0, method)(data1)
        field0.Data               = data0
        space0.dimensions['data'] = data0.order[:]

        # ============================================================
        # 4. Adjust the space of field0 to accommodate its new data
        # ============================================================

        # ------------------------------------------------------------
        # 4a. Remove any size 1 dimensions which are matching
        # dimensions but with different coordinate data array values
        # ------------------------------------------------------------
        field0.squeeze(remove_size1_dims, from_space=True)

        # ------------------------------------------------------------
        # 4b. If broadcasting has grown any size 1 dimensions in
        # space0 then replace their size 1 coordinates with the
        # corresponding size > 1 coordinates from space1.
        # ------------------------------------------------------------
        for dim0 in s['broadcast_dims'] + s['new_dims']:

            dim1                         = dim0_to_dim1[dim0]
            space0.dimension_sizes[dim0] = space1.dimension_sizes[dim1]

            # If it exists, copy the space1 dimension coordinate to
            # space0
            if dim1 in space1:
                space0.insert_coordinate(space1[dim1], dim1_to_dim0,
                                         dim=True,
                                         space=space1, key=dim0)
            #--- End: if

            # Remove from space0 any 1-d auxiliary coordinates for
            # this dimension
            if dim0 in s['aux_coords']:
                for aux0 in s['aux_coords'][dim0]['1-d'].keys():
                    del s['aux_coords'][dim0]['1-d'][aux0]
                    space0.remove_coordinate(aux0)
            #--- End: if

            # Copy to space0 any space1 1-d auxiliary coordinates for
            # this dimension
            if dim1 in v['aux_coords']:
                for coord1 in v['aux_coords'][dim1]['1-d'].itervalues():
                    space0.insert_coordinate(coord1, dim1_to_dim0,
                                             aux=True, dimensions=[dim0],
                                             space=space1)
        #--- End: for

        # ------------------------------------------------------------
        # Consolidate any 1-d auxiliary coordinates for matching
        # dimensions whose defining dimension coordinates have
        # equivalent data arrays.
        #
        # A space0 1-d auxiliary coordinate is retained if there is a
        # corresponding space1 1-d auxiliary with the same standard
        # name and equivalent data array.
        # ------------------------------------------------------------
        for dim0, dim1 in matching_dimensions_with_equivalent_data:

            for aux0, coord0 in s['aux_coords'][dim0]['1-d'].iteritems():
                # Remove this space0 1-d auxiliary coordinate if it
                # has no identity
                if coord0.identity() is None:
                    space0.remove_coordinate(aux0)
                    continue

                # Still here?
                aux0_has_equivalent_pair = False

                for aux1, coord1 in v['aux_coords'][dim1]['1-d'].items():
                    if coord1.identity() is None:
                        continue
                    
                    if coord0._equivalent_data(coord1, transpose=dim1_to_dim0):
                        del v['aux_coords'][dim1]['1-d'][aux1]
                        aux0_has_equivalent_pair = True
                        break
                #--- End: for

                # Remove this space0 1-d auxiliary coordinate if it
                # has no equivalent in space1
                if not aux0_has_equivalent_pair:
                    space0.remove_coordinate(aux0)
        #--- End: for

        # ------------------------------------------------------------
        # Consolidate N-d auxiliary coordinates for matching
        # dimensions which have the same size
        # ------------------------------------------------------------
        # Remove any N-d auxiliary coordinates which span broadcasted
        # dimensions
        for broadcast_dims, aux_coords, space in zip((s['broadcast_dims'], v['broadcast_dims']),
                                                     (s['aux_coords'], v['aux_coords']),
                                                     (space0         , space1         )):
            for dim in broadcast_dims:
                if dim not in aux_coords:
                    continue

                for aux in aux_coords[dim]['N-d']:
                    del aux_coords['N-d'][aux]
                    space.remove_coordinate(aux)
        #--- End: for

        # Remove any N-d auxiliary coordinates which span a mixture of
        # matching and non-matching dimensions
        for aux_coords, space, dim_to_id in zip((s['aux_coords'], v['aux_coords']),
                                                (space0         , space1         ),
                                                (s['dim_to_id'] , v['dim_to_id'] )):
            for aux in aux_coords['N-d'].keys():

                # Count how many of this N-d auxiliary coordinate's
                # dimensions are matching dimensions
                aux_coords['N-d'][aux] = set()
                for dim in space.dimensions[aux]:
                    identity = dim_to_id[dim]
                    if identity in matching_ids:
                        aux_coords['N-d'][aux].add(identity)
                #--- End: for

                n_matching_dims = len(aux_coords['N-d'][aux])
                if 1 <= n_matching_dims < len(space.dimensions[aux]):
                    del aux_coords['N-d'][aux]
                    if space is space0:
                        space.remove_coordinate(aux)
            #--- End: for
        #--- End: for

        # Forget about
        for aux0 in s['aux_coords']['N-d'].keys():
             n_matching_dims = len(s['aux_coords']['N-d'][aux0])
             if n_matching_dims == 0:
                 del s['aux_coords']['N-d'][aux0]
        #--- End: for

        # Copy to space0 any space1 N-d auxiliary coordinates which do
        # not span any matching dimensions
        for aux1, coord1 in v['aux_coords']['N-d'].items():
             n_matching_dims = len(v['aux_coords']['N-d'][aux1])
             if n_matching_dims == 0:
                 del v['aux_coords']['N-d'][aux1]
                 dims = [dim1_to_dim0[dim1] for dim1 in space1.dimensions[aux1]]
                 space0.insert_coordinate(coord1, dim1_to_dim0,
                                          aux=True, dimensions=dims,
                                          space=space1)
        #--- End: for

        # By now, aux_coords0['N-d'] contains only those N-d auxiliary
        # coordinates which span equal sized matching dimensions.

        # Remove from space0 any N-d auxiliary coordinates which span
        # same-size matching dimensions and do not have an equivalent
        # N-d auxiliary coordinate in space1 (i.e. one which spans the
        # same dimensions, has the same standard name and has
        # equivalent data)
        for aux0, coord0 in s['aux_coords']['N-d'].iteritems():

            # Remove space0 N-d auxiliary coordinate if it has no
            # standard name
            if coord0.identity() is None:
                space0.remove_coordinate(aux0)
                continue

            # Still here?
            aux0_has_equivalent_pair = False
            for aux1, coord1 in v['aux_coords']['N-d'].items():
                if coord1.identity() is None:
                    continue

                if coord0._equivalent_data(coord1, transpose=dim1_to_dim0):
                    del v['aux_coords']['N-d'][aux1]
                    aux0_has_equivalent_pair = True
                    break
            #--- End: for

            # Remove space0 N-d auxiliary coordinate if it has no
            # equivalent in space1
            if not aux0_has_equivalent_pair:
                space0.remove_coordinate(aux0)
        #--- End: for

        return field0
    #--- End: def
    
    def _equivalent_data(self, other, rtol=None, atol=None,
                         transpose=None):
        '''

'''
        if not (self.hasData and other.hasData):
            return False
        
        if self.size != other.size:
            return False

        if transpose is None:
            s = self.space.analyse()
            t = other.space.analyse()

            transpose = dict((dim1, s['id_to_dim'][identity])
                             for dim1, identity in t['dim_to_id'].iteritems()
                             if identity in s['id_to_dim'])
            
            if set(transpose) != set(other.space.dimensions['data']):
                return False
        #--- End: if

        return self.Data._equivalent(other.Data,
                                     rtol=rtol, atol=atol,
                                     transpose=transpose,
                                     squeeze=True,
                                     use_directions=True)
    #--- End: def
 
    def _parse_axes(self, axes, method, default=[], dims=False, ignore=False):
        '''

'''
        space           = self.space
        dimensions      = space.dimensions
        data_dimensions = dimensions['data']

        if not axes and axes is not 0:
            axes = default

        # Convert axes to a list of integer positions
        if isinstance(axes, (str, int, long)):
            axes = [axes]
        else:
            axes = list(axes)
        
        axes2 = []
        for axis in axes:
            if axis in data_dimensions:
                axes2.append(data_dimensions.index(axis))
            elif isinstance(axis, (int, long)):
                if axis < 0:
                    axes2.append(axis + self.ndim)
                else:
                    axes2.append(axis)
            else:
                key = self.coord(axis, key=True, exact=True)
        
                if key is None:
                    raise ValueError(
                        "Can't %s: Can't determine %s dimension from '%s'" %
                        (method, axis))
                
                axis = dimensions[key][0] 
                if axis in data_dimensions:
                    axes2.append(data_dimensions.index(axis))
                elif not ignore:
                    # Do not ignore dimensions which don't span the
                    # data array
                    raise ValueError(
                        "Can't %s: '%s' dimension not spanned by data array" %
                        axis, method)
        #--- End: for

        if axes2:
            # Check for duplicate axes
            if len(axes2) != len(set(axes2)):
                raise ValueError("Can't %s: Repeated axis: %s" %
                                 (method, repr(axes2)))
            
            # Check for out of range axes
            if max(axes2) >= self.ndim:
                raise ValueError("Can't %s: Invalid axis for this array: %d" %
                             (method, max(axes2)))
        #--- End: if

        if dims:
            return axes2, [data_dimensions[i] for i in axes2]
        else:
            return axes2
    #--- End: def

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)

'''
        if hasattr(self, 'space'):
            space = self.space
            # Field has a space
            dim_names = []
            for dim in space.dimensions['data']:
                if dim in space:
                    dim_names.append('%s(%d)' % (space[dim].name(ncvar=True,
                                                                 default=dim),
                                                 space.dimension_sizes[dim]))
                else:
                    dim_names.append('%s(%d)' % (dim, 
                                                 space.dimension_sizes[dim]))
            #--- End: for
            dim_names = str(tuple(dim_names))
            dim_names = dim_names.replace("'", "").replace(',)', ')')
        else:
            # Field has no space
            dim_names = str(self.shape).replace(',)', ')')
        #--- End: if
            
        return '<CF Field: %s%s>' % (self.name(long_name=True, ncvar=True,
                                               default=''),
                                     dim_names)
    #--- End: def

    def __str__(self):
        '''
x.__str__() <==> str(x)

'''
        r = repr(self)
        r = r.replace('<CF Field: ', '', 1)
        r = r.replace('>', '')

        string = ['Data            : %s' % r]

        # Field units
        units = getattr(self, 'units', None)
        if units:
            string[0] = '%s %s' % (string[0], units)

        # Cell methods
        if hasattr(self, 'cell_methods'):
            string.append(str(self.cell_methods))

        # Space
        if hasattr(self, 'space'):
            string.append(str(self.space))
        else:
            string.append('Dimensions      : \nAuxiliary coords: \nCell measures   : ')

        return '\n'.join(string) + '\n'
    #--- End def

    # ----------------------------------------------------------------
    # CF property: Conventions
    # ----------------------------------------------------------------
    @property
    def Conventions(self):
        '''

The Conventions CF property.

**Examples**

>>> f.Conventions = 'CF-1.5'
>>> f.Conventions
'CF-1.5'
>>> del f.Conventions

>>> f.setprop('Conventions', 'CF-1.6')
>>> f.getprop('Conventions')
'CF-1.6'
>>> f.delprop('Conventions')

'''
        return self.getprop('Conventions')
    #--- End: def
    @Conventions.setter
    def Conventions(self, value): self.setprop('Conventions', value)
    @Conventions.deleter
    def Conventions(self):        self.delprop('Conventions')

    # ----------------------------------------------------------------
    # Attribute: ancillary_variables
    # ----------------------------------------------------------------
    @property
    def ancillary_variables(self):
        '''

An AncillaryVariables object containing CF ancillary data.

**Examples**

>>> f.ancillary_variables
[<CF Field: >]

'''
        return self._get_special_attr('ancillary_variables')
    #--- End: def
    @ancillary_variables.setter
    def ancillary_variables(self, value):
        self._set_special_attr('ancillary_variables', value)
    @ancillary_variables.deleter
    def ancillary_variables(self): self._del_special_attr('ancillary_variables')

    # ----------------------------------------------------------------
    # Attribute: Flags
    # ----------------------------------------------------------------
    @property
    def Flags(self):
        '''

A Flags object containing self-describing CF flag values.

Stores the flag_values, flag_meanings and flag_masks CF properties in
an internally consistent manner.

**Examples**

>>> f.Flags
<CF Flags: flag_values=[0 1 2], flag_masks=[0 2 2], flag_meanings=['low' 'medium' 'high']>

'''
        return self._get_special_attr('Flags')
    @Flags.setter
    def Flags(self, value):
        self._set_special_attr('Flags', value)
    @Flags.deleter
    def Flags(self):
        self._del_special_attr('Flags')

    # ----------------------------------------------------------------
    # CF property: flag_values
    # ----------------------------------------------------------------
    @property
    def flag_values(self):
        '''

The flag_values CF property.

Stored as a 1-d numpy array but may be set as any array-like object.

**Examples**

>>> f.flag_values = ['a', 'b', 'c']
>>> f.flag_values
array(['a', 'b', 'c'], dtype='|S1')
>>> f.flag_values = numpy.arange(4)
>>> f.flag_values
array([1, 2, 3, 4])
>>> del f.flag_values

>>> f.setprop('flag_values', 1)
>>> f.getprop('flag_values')
array([1])
>>> f.delprop('flag_values')

'''
        try:
            return self.Flags.flag_values
        except AttributeError:
            raise AttributeError(
                "%s doen't have CF property 'flag_values'" %
                self.__class__.__name__)
    #--- End: def
    @flag_values.setter
    def flag_values(self, value):
        try:
            self.Flags.flag_values = value
        except AttributeError:
            self.Flags = Flags(flag_values=value)
    #--- End: def
    @flag_values.deleter
    def flag_values(self):
        try:
            del self.Flags.flag_values
        except AttributeError:
            raise AttributeError(
                "%s doen't have CF property 'flag_values'" %
                self.__class__.__name__)
        else:
            if not self.Flags:
                del self.Flags
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: flag_masks
    # ----------------------------------------------------------------
    @property
    def flag_masks(self):
        '''
The flag_masks CF property.

Stored as a 1-d numpy array but may be set as array-like object.

**Examples**

>>> f.flag_masks = numpy.array([1, 2, 4], dtype='int8')
>>> f.flag_masks
array([1, 2, 4], dtype=int8)
>>> f.flag_masks = (1, 2, 4, 8)
>>> f.flag_masks
array([1, 2, 4, 8], dtype=int8)
>>> del f.flag_masks

>>> f.setprop('flag_masks', 1)
>>> f.getprop('flag_masks')
array([1])
>>> f.delprop('flag_masks')

'''
        try:
            return self.Flags.flag_masks
        except AttributeError:
            raise AttributeError(
                "%s doen't have CF property 'flag_masks'" %
                self.__class__.__name__)
    #--- End: def
    @flag_masks.setter
    def flag_masks(self, value):
        try:
            self.Flags.flag_masks = value
        except AttributeError:
            self.Flags = Flags(flag_masks=value)
    #--- End: def
    @flag_masks.deleter
    def flag_masks(self):
        try:
            del self.Flags.flag_masks
        except AttributeError:
            raise AttributeError(
                "%s doen't have CF property 'flag_masks'" %
                self.__class__.__name__)
        else:
            if not self.Flags:
                del self.Flags
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: flag_meanings
    # ----------------------------------------------------------------
    @property
    def flag_meanings(self):
        '''

The flag_meanings CF property.

Stored as a 1-d numpy string array but may be set as a space delimited
string or any array-like object.

**Examples**

>>> f.flag_meanings = 'low medium      high'
>>> f.flag_meanings
array(['low', 'medium', 'high'],
      dtype='|S6')
>>> f.flag_meanings = ['left', 'right']
>>> f.flag_meanings
array(['left', 'right'],
      dtype='|S5')
>>> f.flag_meanings = 'ok'
>>> f.flag_meanings
array(['ok'],
      dtype='|S2')
>>> del f.flag_meanings

>>> f.setprop('flag_meanings', numpy.array(['a', 'b'])
>>> f.getprop('flag_meanings')
array(['a', 'b'],
      dtype='|S1')
>>> f.delprop('flag_meanings')

'''
        try:
            return self.Flags.flag_meanings
        except AttributeError:
            raise AttributeError(
                "%s doen't have CF property 'flag_meanings'" %
                self.__class__.__name__)
    #--- End: def
    @flag_meanings.setter
    def flag_meanings(self, value):
        try:
            self.Flags.flag_meanings = value
        except AttributeError:
            self.Flags = Flags(flag_meanings=value)
    #--- End: def
    @flag_meanings.deleter
    def flag_meanings(self):
        try:
            del self.Flags.flag_meanings
        except AttributeError:
            raise AttributeError(
                "%s doen't have CF property 'flag_meanings'" %
                self.__class__.__name__)
        else:
            if not self.Flags:
                del self.Flags
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: cell_methods
    # ----------------------------------------------------------------
    @property
    def cell_methods(self):
        '''

The CellMethods object containing the CF cell methods of the data array.

**Examples**

>>> f.cell_methods
<CF CellMethods: time: mean (interval: 1.0 month)>

'''
        value = self._get_special_attr('cell_methods')
#        if isinstance(value, basestring):
#            cm = CellMethods()
#            value = cm.parse(value, field=self)
        return value
    #--- End: def

    @cell_methods.setter
    def cell_methods(self, value):
        self._set_special_attr('cell_methods', value)

    @cell_methods.deleter
    def cell_methods(self):
        self._del_special_attr('cell_methods')

    # ----------------------------------------------------------------
    # CF property: institution (a simple attribute)
    # ----------------------------------------------------------------
    @property
    def institution(self):
        '''

The institution CF property.

**Examples**

>>> f.institution = 'University of Reading'
>>> f.institution
'University of Reading'
>>> del f.institution

>>> f.setprop('institution', 'University of Reading')
>>> f.getprop('institution')
'University of Reading'
>>> f.delprop('institution')

'''
        return self.getprop('institution')
    #--- End: def
    @institution.setter
    def institution(self, value): self.setprop('institution', value)
    @institution.deleter
    def institution(self):        self.delprop('institution')

    # ----------------------------------------------------------------
    # CF property: references (a simple attribute)
    # ----------------------------------------------------------------
    @property
    def references(self):
        '''

The references CF property.

**Examples**

>>> f.references = 'some references'
>>> f.references
'some references'
>>> del f.references

>>> f.setprop('references', 'some references')
>>> f.getprop('references')
'some references'
>>> f.delprop('references')

'''
        return self.getprop('references')
    #--- End: def
    @references.setter
    def references(self, value): self.setprop('references', value)
    @references.deleter
    def references(self):        self.delprop('references')

    # ----------------------------------------------------------------
    # CF property: standard_error_multiplier	
    # ----------------------------------------------------------------
    @property
    def standard_error_multiplier(self):
        '''

The standard_error_multiplier CF property.

**Examples**

>>> f.standard_error_multiplier = 2.0
>>> f.standard_error_multiplier
2.0
>>> del f.standard_error_multiplier

>>> f.setprop('standard_error_multiplier', 2.0)
>>> f.getprop('standard_error_multiplier')
2.0
>>> f.delprop('standard_error_multiplier')

'''
        return self.getprop('standard_error_multiplier')
    #--- End: def

    @standard_error_multiplier.setter
    def standard_error_multiplier(self, value):
        self.setprop('standard_error_multiplier', value)
    @standard_error_multiplier.deleter
    def standard_error_multiplier(self):
        self.delprop('standard_error_multiplier')

    # ----------------------------------------------------------------
    # CF property: source	
    # ----------------------------------------------------------------
    @property
    def source(self):
        '''

The source CF property.

**Examples**

>>> f.source = 'radiosonde'
>>> f.source
'radiosonde'
>>> del f.source

>>> f.setprop('source', 'surface observation')
>>> f.getprop('source')
'surface observation'
>>> f.delprop('source')

'''
        return self.getprop('source')
    #--- End: def

    @source.setter
    def source(self, value): self.setprop('source', value)
    @source.deleter
    def source(self):        self.delprop('source')

    # ----------------------------------------------------------------
    # CF property: title
    # ----------------------------------------------------------------
    @property
    def title(self):
        '''

The title CF property.

**Examples**

>>> f.title = 'model data'
>>> f.title
'model data'
>>> del f.title

>>> f.setprop('title', 'model data')
>>> f.getprop('title')
'model data'
>>> f.delprop('title')

'''
        return self.getprop('title')
    #--- End: def

    @title.setter
    def title(self, value): self.setprop('title', value)
    @title.deleter
    def title(self):        self.delprop('title')

    # ----------------------------------------------------------------
    # Attribute: space
    # ----------------------------------------------------------------
    @property
    def space(self):
        '''

The Space object containing the coordinate system (space).

**Examples**

>>> f.space
<CF Space: (12, 19, 73, 96)>

'''
        return self._get_special_attr('space')
    #--- End: def
    @space.setter
    def space(self, value):
        self._set_special_attr('space', value)
    #--- End: def
    @space.deleter
    def space(self):
        self._del_special_attr('space')
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: subspace (read only)
    # ----------------------------------------------------------------
    @property
    def subspace(self):
        '''

Return a new field whose data and space are subspaced.

This attribute may be indexed to select a subset from dimension index
values (square brackets: ``f.subspace[indices]``) or called to select a
subset from dimension coordinate array values (round brackets:
``f.subspace(coordinate_values)``)

**Subspacing by indexing**

Subsetting by dimension indices uses an extended Python slicing
syntax, which is similar numpy array indexing. There are two
extensions to the numpy indexing functionality:

* Size 1 dimensions are never removed.

  An integer index i takes the i-th element but does not reduce the
  rank of the output array by one.

* When advanced indexing is used on more than one dimension, the
  advanced indices work independently.

  When more than one dimension's slice is a 1-d boolean array or 1-d
  sequence of integers, then these indices work independently along
  each dimension (similar to the way vector subscripts work in
  Fortran), rather than by their elements.

**Subspacing by coordinate values**

Subsetting by coordinate values allows a subsetted field to be defined
by particular coordinate values of its space.

Subsetting by coordinate values is functionally equivalent to
subsetting by indexing - internally, the selected coordinate values
are in fact converted to dimension indices.

Coordinate values are provided as arguments to a call to the subset
method.

The benefits to subsetting in this fashion are:

* The dimensions to be subsetted are identified by name.

* The position in the data array of each dimension need not be
  known.

* Dimensions for which no subsetting is required need not be
  specified.

* Size 1 dimensions of the space which are not spanned by the data
  array may be specified.

**Examples**

'''
        return SubspaceField(self)
    #--- End: def

    def chunk(self, chunksize=None, extra_boundaries=None, chunk_dims=None):
        '''

Partition the field in place for LAMA functionality.

:Parameters:

    chunksize : int, optional

    extra_boundaries : sequence of lists or tuples, optional

    chunk_dims : sequence of lists or tuples, optional

:Returns:

    extra_boundaries, chunk_dims : {list, list}

'''

#### With the new aggregation algorithm we probabalby don't need to do
#### this - we can just inherit from Variable !!!!!!!!!!!!!!!!!!!!!!!!
#### because it ceases to matter if partitions are in different places
#### in coordinate and teh field (I think .... ?) ????

        if not chunksize:
            chunksize = CHUNKSIZE()

        extra_boundaries, chunk_dims = self.Data.chunk(chunksize,
                                                       extra_boundaries,
                                                       chunk_dims)

        if not extra_boundaries:
            return extra_boundaries, chunk_dims

        # Still here?
        space      = self.space
        dimensions = space.dimensions

        for key, variable in space.iteritems():
            variable_dimensions = dimensions[key]
            for extra_bounds, dim in zip(extra_boundaries, chunk_dims):
                if dim in variable_dimensions:
                    # This variable spans the chunking dimension, so
                    # chunk it with the same new partition locations
                    # as the field's data
                    variable.chunk(None, [extra_bounds], [dim])
        #--- End: for

        return extra_boundaries, chunk_dims
    #--- End: def

    def finalize(self, chunksize=None):
        '''

Finalize a newly created field.

It is essential that this is carried out on every new field to ensure
that methods and functions of fields work correctly. Finalization
entails:

* Expanding scalar coordinate and cell measures to 1-d.

* Setting the direction of the space's dimensions.

* Conforming the space's internal dimension names.

* Partitioning the data arrays for LAMA functionality.

:Parameters:

    chunksize : int, optional
        Set the maximum size in bytes of sub-arrays for data storage
        and processing. By default the size used by the `CHUNKSIZE`
        function is used.

:Returns:

    None

**Examples**

>>> f.finalize()
>>> f.finalize(2**30)

'''
        space           = self.space
        dimensions      = space.dimensions
        data_dimensions = dimensions['data']

        # ------------------------------------------------------------
        # Find the direction of each dimension
        # ------------------------------------------------------------
        directions = {}
        for dim in space.dimension_sizes:
            if dim in space:
                directions[dim] = space[dim]._get_direction()
            else:
                directions[dim] = True
        #--- End: for

        self._set_Data_attributes(data_dimensions, directions)

        for key, variable in space.iteritems():
            variable._set_Data_attributes(dimensions[key], directions)

        # ------------------------------------------------------------
        # Turn scalar coordinates and cell measures into size 1, 1-d
        # ------------------------------------------------------------
        for key, variable in space.iteritems():
            if variable.isscalar:
                dim = dimensions[key][0]
                variable.expand_dims(0, dim, variable.Data.direction)
        #--- End: for

        # ------------------------------------------------------------
        # Make sure that each partition is no larger than the chunk
        # size
        # ------------------------------------------------------------
        self.chunk(chunksize)
    #--- End: def

    def coord(self, arg,
              role=None, key=False, exact=False, dim=False, one_d=False, 
              maximal_match=True):
        '''

Return a coordinate of the field.

Note that the returned coordinate is an object identity to the
coordinate stored in the space so, for example, a coordinate's
properties may be changed in-place:

>>> f.coord('height').long_name
AttributeError: 'Coordinate' object has no CF property 'long_name'
>>> f.coord('hei').long_name = 'HEIGHT'
>>> f.coord('heigh').long_name
'HEIGHT'

Or a deep copy may be made:

>>> c = f.coord('height').copy()

:Parameters:

    arg : str or dict or int
        The identify of the coordinate. One of:

        * A string containing (an abbreviation) of its standard name
          (such as 'time').

        * A string containing its identifier in the field's space
          (such as 'dim2').

        * A dictionary containing one or more (abbreviations of )
          property names and their values as its key/value pairs (such
          as {'long_name': 'something'}). If two or more properties
          are specified then the returned coordinate must satisfy all
          criteria, unless `maximal_match` is False in which case the
          returned coordinate will satisfy all criteria at least one
          of the criteria.

        * An integer given the position of a dimension in the field's
          data array.

    exact : bool, optional
        If True then assume that the value of a name given by `arg` is
        exact. By default it is considered to be an abbreviation.

    key : bool, optional
        Return the field's space's identifier for the coordinate.

    role : bool, optional
        Restrict the search to coordinates of the given role. Valid
        values are 'dim' and 'aux', for dimension and auxiliary
        coordinate constructs. By default both types are considered.

:Returns:

    out : Coordinate or str or None
        The requested coordinate, or the field's space's identifier
        for the coordinate, or `None` if no coordinate could be found.

**Examples**

>>> f.coord('lon')
<CF Coordinate: longitude(128)>
>>> f.coord('lon', key=True)
'dim2'
>>> f.coord('lonX', key=True)
None
>>> f.coord('lon', exact=True)
None
>>> f.coord('longitude', exact=True)
<CF Coordinate: longitude(128)>

'''
        if hasattr(self, 'space'):
            return self.space.coord(arg,
                                    role=role, key=key, exact=exact,
                                    dim=dim, one_d=one_d,
                                    maximal_match=maximal_match)
        #--- End: if

        return None
    #--- End: def

    def copy(self, _omit_Data=False):
        '''

Return a deep copy.

Equivalent to ``copy.deepcopy(f)``.

:Returns:

    out :
        The deep copy.

**Examples**

>>> g = f.copy()

'''
        new = super(Field, self).copy(_omit_Data=_omit_Data,
                                      _omit_special=('space',))

        if hasattr(self, 'space'):
            new.space = self.space.copy()

        return new
    #--- End: def

    def dump(self, id=None):
        '''

Return a string containing a full description of the instance.

:Parameters:

    id : str, optional
        Set the common prefix of component names. By default the
        instance's class name is used.

:Returns:

    out : str
        A string containing the description.

**Examples**

>>> x = f.dump()
>>> print f.dump()
>>> print f.dump(id='pressure_field')

'''
        if id is None:
            id = self.__class__.__name__

        # Produce summary
        string = ['field summary\n-------------']
        string.append(str(self))
        string.append('')

        # Produce dump
        name = self.name(default='', long_name=True)
        string.append('%s field' % name)
        string.append(''.ljust(len(string[-1]), '-'))

        string.append(super(Field, self).dump(id=id, omit=('Flags',
                                                           'space',)))

        if hasattr(self, 'Flags'):
            string.append(self.Flags.dump(id='%(id)s' % locals()))

        if hasattr(self, 'space'):
            string.append(self.space.dump(id='%(id)s.space' % locals()))

        return '\n'.join(string)
    #--- End: def

    def equals(self, other, rtol=None, atol=None, traceback=False):
        '''

True if two fields are logically equal, False otherwise.

The Conventions CF property is ignored in the comparison.

:Parameters:

    other :
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

    traceback : bool, optional
        If True then print a traceback highlighting where the two
        instances differ.

:Returns: 

    out : bool
        Whether or not the two instances are equal.

**Examples**

>>> f.Conventions
'CF-1.0'
>>> g = f.copy()
>>> g.Conventions = 'CF-1.5'
>>> f.equals(g)
True

In the following example, two fields differ only by the long name of
their time coordinates. The traceback shows that they differ in their
spaces, that they differ in their time coordinates and that the long
name could not be matched.

>>> g = f.copy()
>>> g.coord('time').long_name = 'something_else'
>>> f.equals(g, traceback=True)
Coordinate: Different long_name: 'time', 'something else'
Coordinate: Different long_name: 'time', 'latitude in rotated pole grid'
Coordinate: Different long_name: 'time', 'longitude in rotated pole grid'
Space: Different coordinate: <CF Coordinate: time(12)>
Field: Different 'space': <CF Space: (73, 96, 12)>, <CF Space: (73, 96, 12)>
False

'''
        return super(Field, self).equals(other, rtol=rtol, atol=atol, 
                                         traceback=traceback,
                                         ignore=('Conventions',))
    #---End: def

    def expand_dims(self, arg=None, axis=0):
        '''

Expand the shape of the data array in place.

Insert a new size 1 axis, corresponding to a given position in the
data array shape.

:Parameters:

    arg : str, optional
        The dimension to insert. if specified, the dimension should
        exist in the field's space and is identified by its standard
        name or by the space's internal dimension name. By default,
        insert a new dimension which doesn't yet exist in the field's
        space.

    axis : int, optional
        Position (amongst axes) where new axis is to be inserted. By
        default, insert at position 0.

:Returns:

    None

**Examples**

>>> f.expand_dims()
>>> f.expand_dims(axis=1)
>>> f.expand_dims('height')
>>> f.expand_dims('height', 3)
>>> f.expand_dims('dim1', 3)

'''
        space           = self.space
        dimension_sizes = space.dimension_sizes
        dimensions      = space.dimensions
        data_dimensions = dimensions['data']

        if arg is None:
            dim = space.new_dimension()

        elif arg in dimension_sizes:
            if dimension_sizes[arg] != 1:
                raise ValueError(
                    "Can't insert a dimension with size > 1 into %s: '%s' (size %d)" %
                    (self.__class__.__name__, arg, dimension_sizes[arg]))

            if arg in data_dimensions:
                raise ValueError(
                    "Can't insert a duplicate dimension into %s: '%s'" %
                    (self.__class__.__name__, arg))

            dim = arg

        else:
            key = self.coord(arg, key=True, exact=True)

            if key is None or len(dimensions[key]) > 1:
                raise ValueError("9889asdasdasdaaaaaas 98123 gto dch axis")

            dim = dimensions[key][0]

            if dimension_sizes[dim] > 1:
                raise ValueError(
                    "Can't insert into %s Data a dimension with size > 1: '%s' ('%s') (size %d)" %
                    (self.__class__.__name__, dim, arg, dimension_sizes[dim]))

            if dim in data_dimensions:
                raise ValueError(
                    "Can't insert into %s Data a duplicate dimension: '%s' ('%s')" %
                    (self.__class__.__name__, dim, arg))
        #--- End: if

        super(Field, self).expand_dims(axis, dim, space.direction(dim))

        data_dimensions.insert(axis, dim)

        # Make sure that dimension_sizes is up to date
        if dim not in dimension_sizes:
            dimension_sizes[dim] = 1

        return dim
    #--- End: def

    def indices(self, **kwargs):
        '''

Return the indices to the data array which correspond to coordinate
values.

Any index for a dimension of the data array for which no coordinates
are specified is returned as a full slice (i.e. ``slice(None)``).

Size 1 coordinates for dimensions of the space which are not spanned
by the data array may be specified, but no corresponding index will be
returned.

:Parameters:

    kwargs : optional
        Keyword names identify coordinates; and keyword values specify
        the coordinate values which are to be interpreted as indices
        to the field's data array.

        A keyword name identifies a coordinate by either its
        `standard_name` property, its `identity` attribute or its
        identifier in the field's space, in that order of precedence.

        A keyword value is a condition, or sequence of conditions,
        which is evaluated by finding where the coordinate's data
        array equals each condition. The locations where the
        conditions are satisfied are interpreted as indices to the
        field's data array. If a condition is a scalar, ``x``, then
        this is equivalent to the Comparison object ``cf.eq(x)``.

:Returns:

    out : tuple
        
**Examples**

>>> f.slice(lat=0.0, lon=0.0)
<CF Field: air_temperature(12, 1, 1)
>>> f.slice(lon=cf.lt(0.0), lon=[0, 2.8125])
<CF Field: air_temperature(12, 32, 2)
>>> f.slice(lon=[0, 1])
ValueError: 'longitude' coordinate doesn't contain the value 1
>>> f.slice(lon=1)
ValueError: No indices found for 'longitude' coordinate

'''
        if not kwargs:
            return (slice(None),) * self.ndim
        
        space = self.space

        dimensions = []

        # Initialize index
        indices = [slice(None)] * self.ndim

        # Loop round slice criteria
        for name, value in kwargs.iteritems():

            key = self.coord(name, key=True, exact=True)

            if key is None:
                raise ValueError(
                    "Can't identify coordinate '%(name)s'" % locals())

            c = space[key]

            if key.startswith('dim'):
                # Dimension coordinate
                dim = key
            else:
                # Auxiliary coordinate
                dim = space.dimensions[key][0]

                if c.ndim > 1:
                    raise ValueError(
                        "Can't get an index from multidimensional coordinate: '%s'" %
                        c.name())
            #--- End: if

            if dim in dimensions:
                raise ValueError("Two coordinates share the same dimension")

            dimensions.append(dim)

            coord_match = None

            if not isinstance(value, (tuple, list)):
                value = (value,)

            for v in value:
                c_match = (c.Data == v)
                if not c_match.any():
                    continue

                if coord_match is None:
                    coord_match = c_match
                else:
                    coord_match = c_match | coord_match
            #--- End: for

            if coord_match is None:
                raise IndexError(
                    "No indices found for '%s' values %s" %
                    (c.name(), v))

            if dim not in space.dimensions['data']:
                continue

            # Still here? Then add the index to the right place in the
            # list of indices.
            pos          = space.dimensions['data'].index(dim)
            indices[pos] = coord_match.array
        #--- End: for

        return tuple(parse_indices(self, tuple(indices)))
    #--- End: def

    def match(self, prop={}, attr={}, coord={}, cellsize={}):
        '''

Determine whether or not a field satisfies conditions.

Conditions may be specified on the field's CF properties, attributes,
coordinate values and coordinate cell sizes.

:Parameters:

    prop : dict, optional
        Dictionary for which each key/value pair is a CF property name
        and a condition for the property to be tested against. A match
        occurs if the CF property value equals the condition. The
        condition may be a scalar or a `Comparison` object. A scalar
        condition, ``x``, is equivalent to the Comparison object
        ``cf.eq(x)``.

        Note that:

        * If the condition contains a regular expression pattern as
          recognised by the `re` module then special characters for
          the start and end of the string are assumed and need not be
          included. For example, '.*wind' is equivalent to '^.*wind$'.

    attr : dict, optional
        Dictionary for which each key/value pair is an attribute name
        and a condition for the attribute to be tested against. A
        match occurs if the attribute value equals the condition. The
        condition may be a scalar or a `Comparison` object. A scalar
        condition, ``x``, is equivalent to the Comparison object
        ``cf.eq(x)``.
  
        Note that:


        * If the condition contains a regular expression pattern as
          recognised by the `re` module then special characters for
          the start and end of the string are assumed and need not be
          included. For example, '.*wind' is equivalent to '^.*wind$'.

        * For the `Units` attribute, the condition is passed if the
          attribute is equivalent (rather than equal) to the
          object. (Note that this behaviour does not apply to the
          `units` attribute.)

    coord : dict, optional
        Dictionary for which each key/value pair is a coordinate
        identity and a condition for the coordinate to be tested
        against. A match occurs if at least one element of the
        coordinate's data array equals the condition. The condition
        may be a scalar or a `Comparison` object. A scalar condition,
        ``x``, is equivalent to the Comparison object ``cf.eq(x)``.

    cellsize : dict, optional
        Dictionary for which each key/value pair is a coordinate
        indentity and a condition for the coordinate's cell sizes to
        be tested against. If the coordinate has not been specified
        with the `coord` parameter, then a match occurs if all of the
        coordinate's cell sizes equal the condition, otherwise a match
        occurs if all of the cells meeting the `coord` condition equal
        the cell size condition. The condition may be a scalar or a
        `Comparison` object. A scalar condition, ``x``, is equivalent
        to the Comparison object ``cf.eq(x)``.

:Returns:

    out : bool
        True if the field satisfies the given criteria, False
        otherwise.

**Examples**

>>> print f
Data            : air_temperature(time, latitude, longitude)
Cell methods    : time: mean
Dimensions      : time(12) = [15, ..., 345] days since 1860-1-1
                : latitude(73) = [-90, ..., 90] degrees_north
                : longitude(96) = [0, ..., 356.25] degrees_east
                : height(1) = [2] m

>>> f.match(prop={'standard_name': 'air_temperature'})
True        
>>> f.match(prop={'standard_name': ['air_temperature']})
True        
>>> f.match(prop={'standard_name': cf.set(['air_temperature', 'air_pressure'])})
True        
>>> f.match(prop={'standard_name': '.*temperature.*'})
True        
>>> f.match(prop={'standard_name': cf.set(['.*temperature.*', 'air_pressure'])})
True        
>>> f.match(prop={'standard_name': '.*pressure.*'})
False       
            
>>> f.match(prop={'Units': 'K'})
True        
>>> f.match(prop={'Units': cf.Units('1.8 K @ 459.67')})
True        
>>> f.match(prop={'Units': cf.set([cf.Units('Pa'), 'K'])})
True        
>>> f.match(prop={'Units': cf.Units('Pa')})
False       
            
>>> f.match(prop={'cell_methods': 'time: mean'})
True        
>>> f.match(prop={'cell_methods': cf.CellMethods('time: mean')})
True
>>> f.match(attr={'cell_methods': ['time: mean', 'time: max']})
True
>>> f.match(prop={'cell_methods': cf.CellMethods('time: max')})
False
>>> f.match(prop={'cell_methods': 'time: mean time: min')})
False

>>> f.match(coord={'latitude': 0})
False
>>> f.match(coord={'latitude': cf.set([0, cf.gt(30)]})
True

>>> f.match(cellsize={'time': cf.wi(28, 31, 'days')})
True

'''
        prop = prop.copy()

        # ------------------------------------------------------------
        # Try to match other cell methods
        # ------------------------------------------------------------
        cell_methods = prop.pop('cell_methods', None)

        if cell_methods is not None:
            if not hasattr(self, 'cell_methods'):
                return False

            if not isinstance(cell_methods, (tuple, list)):
                cell_methods = (cell_methods,)

            match = False
            for v in cell_methods:
                if isinstance(v, basestring):
                    v = CellMethods(v)

                if self.cell_methods.has_cellmethods(v):
                    match = True
                    break
                else:
                    continue
            #--- End: for
            if not match:
                return False
        #--- End: if

        # ------------------------------------------------------------
        # Try to match other properties and attributes
        # ------------------------------------------------------------
        if not super(Field, self).match(prop=prop, attr=attr):
            return False

        # Still here? Then the properties and attributes have matched.

        # ------------------------------------------------------------
        # Try to match coordinate values
        # ------------------------------------------------------------
        
        # Keep a note of found coordinates to save us possibly having
        # to look them up again if there are cellsize conditions
        coords = {}

        if coord:
            # Some coordinate conditions have been been specified

            # Loop round coordinate criteria
            for identity, condition in coord.iteritems():
                if condition is None:
                    continue

                c = self.coord(identity, exact=True)
                if not c:
                    # The field does not have a coordinate with this
                    # identity, so it does not match.
                    return False

                coord_match = (c.Data == condition)

                if not coord_match.any():
                    return False

                coords[identity] = (c, coord_match)
            #--- End: for
        #--- End: if

        # ------------------------------------------------------------
        # Try to match coordinate cell sizes
        # ------------------------------------------------------------
        if cellsize:
            # Some coordinate conditions have been been specified

            for identity, condition in cellsize.iteritems():
                if condition is None:
                    continue

                if identity in coords:
                    c, coord_match = coords[identity]
                else:
                    c = self.coord(identity, exact=True)
                    if not c:
                        # The field does not have a coordinate with
                        # this name, so it does not match.
                        return False            
                #--- End: if

                if not c.isbounded:
                    if condition == 0: # dch this need work - what if condition is Comparison?
                        continue
                    else:
                        # The coordinate does not have any bounds and
                        # the specified cell size is not zero, so it
                        # does not match.
                        return False
                #--- End: if

                b = c.bounds.Data
                cell_sizes = abs(b[...,1] - b[...,0])
                cell_sizes.squeeze('bounds')

                bounds_match = (cell_sizes == condition)

                if identity in coords: 
                    # Coordinate conditions have also been specified,
                    # so return False if any of the matching
                    # coordinate cells do not match the cell size
                    # condition.
                    if not ((bounds_match & coord_match) == coord_match).all():
                        return False

                else:
                    # No coordinate conditions have been specified, so
                    # return False if any coordinate cells do not
                    # match the cell size condition.
                    if not bounds_match.all():
                        return False

            #--- End: for
        #--- End: if

        return True
    #--- End: def

    def flip(self, axes=None):
        '''

Flip dimensions of the data array and space in place.

:Parameters:

    axes : sequence, optional
        The dimensions to be flipped. By default all dimensions are
        flipped. Dimensions to be flipped may be identified with one
        of, or a sequence of any combination of:

            * A dimension's standard name.
            * The integer position of a dimension in the data array.
            * The field's space's internal name of a dimension

:Returns:

    None

**Examples**

>>> f.flip()
>>> f.flip('time')
>>> f.flip(1)
>>> f.flip('dim2')
>>> f.flip(['time', 1, 'dim2'])

'''
        if axes is not None:
            axes = self._parse_axes(axes, 'flip', ignore=True)

        # Flip the requested dimensions
        axes = super(Field, self).flip(axes)

        # Flip any coordinate and cell measures which span the flip
        # dimensions
        space = self.space
        dimensions   = self.space.dimensions['data']
        flip_dims = [dimensions[i] for i in axes]

        dimensions = space.dimensions

        for key, variable in space.iteritems():
            axes = [i
                    for i, dim in enumerate(dimensions[key])
                    if dim in flip_dims]
            if axes:
                variable.flip(axes)
    #--- End: def

    def setitem(self, value, indices=Ellipsis, condition=None, masked=None,
                ref_mask=None, hardmask=None):
        '''

Set selected elements of the data array in place.

The value to which the selected elements of the data array will be set
may be any object which is broadcastable across the selected elements,
but some options insist that the value is logically scalar (i.e. a
scalar or an array with size 1).

A subspace of elements may be selected by indices of the data array or
by locations where coordinates satisfy given conditions. If a further
selection method is included (such as 'only set elements whose data
array value is less than zero'), then this will apply only to the
previously selected subspace.

If the data array's mask is to be treated as a hard mask then it will
remain unchanged after the assignment with the one exception of the
assignment value being `cf.masked`, in which case selected unmasked
elements will always be masked.

If and only if the value to be assigned is `cf.masked` then the
selected elements of data array's *mask* will be set to True
(masked). This is consistent with the behaviour of numpy masked
arrays.

Note the following equivalences and alternative means of assignment:

* ``f.subspace[...]=value`` is equivalent to ``f.setitem(value)``.

* ``f.subspace[indices]=value`` is equivalent to ``f.setitem(value,
  indices)``.

* ``f.subspace[f.indices(**coord_conditions)]=value`` is equivalent
  to ``f.setitem(value, coord_conditions)`` .

* ``f.setmask(True, indices)`` is equivalent to ``f.setitem(cf.masked,
  indices)``.

* ``a=f.varray; a[indices]=value`` is equivalent to ``f.setitem(value,
  indices)``. Note that the `varray` method will fail if there is
  insufficient memory for the numpy array `a`, but the `setitem`
  method will always work.

:Parameters:

    value : array-like
        The value to which the selected elements of the data array
        will be set. Must be an object which is broadcastable across
        the selected elements, unless the selection method insists
        that `value` is logically scalar.

    indices : optional
        Select elements of the mask to be set. Either as:

            * Indices as would be accepted by ``f.subspace``.

            * A dictionary of coordinate identifiers and conditions on
              their data array values as would be accepted as keyword
              arguments to ``f.indices``.
 
        Only elements of the data array described by `indices`, and
        where other conditions (if any) are met, are set to
        `value`. How masked elements of the data array are treated
        depends on the `hardmask` parameter. The `value` must be any
        object broadcastable across the shape implied by `indices`. By
        default, the entire data array is considered

    condition : scalar or Comparison, optional
        A condition applied to each element of the data array
        specified by `indices` which determines whether or not that
        element is set to the logically scalar `value`. The condition
        is evaluated by checking if each element equals the condition,
        and if it does then that element is set to `value`. How masked
        elements of the data array are treated depends on the
        `hardmask` parameter. If `condition` is a scalar, ``x``, then
        this is equivalent to the Comparison object ``cf.eq(x)``. by
        default there is no selection by data array condition.

    masked : bool, optional
        If False then each element of the data array specified by
        `indices` which is unmasked is set to the logically scalar
        `value`. If True then each element of the data array specified
        by `indices` which is masked is unmasked and set to the
        logically scalar `value`, regardless of the `hardmask` is
        parameter. By default no there is no selection by data array
        mask.

    ref_mask : array-like, optional
        Each element of the data array specified by `indices` and
        which corresponds to an element which evaluates to False from
        the reference mask `ref_mask` is set to the logically scalar
        `value`. `ref_mask` may be any object which is broadcastable
        across the shape implied by `indices`. How masked elements of
        the data array are treated depends on the `hardmask`
        parameter. By default there is no seelction by reference mask.

    hardmask : bool, optional
        If True then any selected elements of the data array which are
        masked *will not* be unmasked and assigned to. If False then
        any selected elements of the data array which are masked
        *will* be unmasked and assigned to. By default, the value of
        the instance's `hardmask` attribute is used.

:Returns:

    None

**Examples**

>>> f.setitem(273.15)
>>> f.setitem(273.15, dict(longitude=cf.wi(210, 270, 'degrees_east'),
                           latitude=cf.wi(-5, 5, 'degrees_north'))
>>> f.setitem(1, masked=False)
>>> f.setitem(2, ref_mask=g)

'''
        if isinstance(indices, dict):
            indices = self.indices(indices)

        if not isinstance(value, Variable):
            if value is not numpy.ma.masked:
                if not isinstance(value, Data):
                    value = Data(value, units=self.Units)
                    
                if value.size > 1:
                    raise ValueError("8787")
            #--- End: if

        elif isinstance(value, self.__class__):
           value = _conform_for_assignment(self, value, 'setmask')
           value = value.Data

        elif value.size > 1:
            raise ValueError("8787----6766666667") 
        #--- End: if

        # ------------------------------------------------------------
        # Set elements of self.Data
        # ------------------------------------------------------------
        self.Data.setitem(value,
                          indices=indices,
                          condition=condition,
                          masked=masked,
                          ref_mask=ref_mask,
                          hardmask=hardmask)
    #--- End: def

    def setmask(self, value, indices=Ellipsis):
	'''

Set selected elements of the data array's mask in place.

The value to which the selected elements of the mask will be set may
be any object which is broadcastable across the selected elements. The
broadcasted value may be of any data type but will be evaluated as
boolean.

The mask may be effectively removed by setting every element to False
with ``f.setmask(False)``.

Unmasked elements are set to the fill value.

Note that if and only if the value to be assigned is logically scalar
and evaluates to True then ``f.setmask(value, indices)`` is equivalent
to ``f.setitem(cf.masked, indices)``. This is consistent with the
behaviour of numpy masked arrays.

:Parameters:

    value : array-like
        The value to which the selected element s of the mask will be
        set. Must be an object which is broadcastable across the
        selected elements. The broadcasted value may be of any data
        type but will be evaluated as boolean.

    indices : optional
        Select elements of the mask to be set. Either as:

            * Indices as would be accepted by ``f.subspace``.

            * A dictionary of coordinate identifiers and conditions on
              their data array values as would be accepted as keyword
              arguments to ``f.indices``.
 
        Only elements of the mask described by `indices` are set with
        `value`. By default, the entire mask is set.

:Returns:

    None

**Examples**

'''
        if not isinstance(value, Variable):
            if not isinstance(value, Data):
                value = Data(value, units=self.Units)
                
            if value.size > 1:
                raise ValueError("2323")

        elif isinstance(value, self.__class__):
           value = _conform_for_assignment(self, value, 'setmask')
           value = value.Data

        elif value.size > 1:
            raise ValueError("2323----6767") 
        #--- End: if

        if isinstance(indices, dict):
            indices = self.indices(indices)

        # ------------------------------------------------------------
        # Set elements of self.Data.mask
        # ------------------------------------------------------------
        self.Data.setmask(value, indices=indices)
    #--- End: def

    def spans(self, identities, spans_all=False):
        '''
'''
        s = self.space.analyse()
        
        if not spans_all:
            for identity in identities:
                if identity in s['id_to_dim']:
                    return True

            return False
        else:
            for identity in identities:
                if identity not in s['id_to_dim']:
                    return False

            return True
    #--- End: def

    def squeeze(self, axes=None, from_space=False):
        '''

Remove size 1 dimensions from the field's data array in place.

:Parameters:

    axes : optional
        The size 1 axes to remove. By default, all size 1 axes are
        removed. Size 1 axes for removal may be identified with one
        of, or a sequence of any combination of:

            * A dimension's standard name.
            * The integer position of a dimension in the data array.
            * The field's space's internal name of a dimension
:Returns:

    None

**Examples**

>>> f.squeeze()
>>> f.squeeze('time')
>>> f.squeeze(1)
>>> f.squeeze('dim2')
>>> f.squeeze([1, 'time', 'dim2'])
>>> f.squeeze('height', from_space=True)

'''
        if axes is not None:
            axes = self._parse_axes(axes, 'squeeze', ignore=True)

        # Squeeze the data
        axes = super(Field, self).squeeze(axes)
        
        # Remove the squeezed dimensions from the space's list of
        # data dimensions
        if axes:            
            data_dimensions = self.space.dimensions['data']

            if from_space:
                dims = [data_dimensions[i] for i in axes]
                self.space.squeeze(dims)
            #--- End: if

            data_dimensions[:] = [dim
                                  for i, dim in enumerate(data_dimensions)
                                  if i not in axes]
        #--- End: if

        return axes
    #--- End: def

    def transpose(self, axes=None):
        '''

Permute the dimensions of the data array in place.

:Parameters:

    axes : sequence, optional
        The new order of the data array. By default, reverse the
        dimensions' order, otherwise the axes are permuted according
        to the values given. The values of the sequence may be any
        combination of:

            * A dimension's standard name.
            * The integer position of a dimension in the data array.
            * The field's space's internal name of a dimension.

:Returns:

    None

**Examples**

>>> f.ndim
3
>>> f.transpose()
>>> f.transpose(['latitude', 'time', 'longitude'])
>>> f.transpose([1, 0, 2])
>>> f.transpose((1, 'time', 'dim2'))

'''
        ndim  = self.ndim
        
        if ndim <= 1:
            return
        
        if axes is None:
            # By default, reverse the dimensions
            axes = range(ndim-1, -1, -1)
        else:
            axes = self._parse_axes(axes, 'transpose')

            # Return unchanged if axes are in the same order as the data
            if axes == range(ndim):
                return

            if len(axes) != ndim:
                raise ValueError(
                    "Can't transpose: Axes don't match array: %s" % (axes,))
        #--- End: if

        # Transpose the field
        super(Field, self).transpose(axes)

        # Reorder the list of dimensions in the space
        dimensions    = self.space.dimensions['data']
        dimensions[:] = [dimensions[i] for i in axes]
    #--- End: def

    def unsqueeze(self):
        '''

Insert size 1 dimensions from the field's space into its data array in
place.

All size 1 dimensions which are not already spanned by the field's
data array are inserted.

:Returns:

    None

**Examples**

>>> print f
Data            : air_temperature(time, latitude, longitude)
Cell methods    : time: mean
Dimensions      : time(1) = [15] days since 1860-1-1
                : latitude(73) = [-90, ..., 90] degrees_north
                : longitude(96) = [0, ..., 356.25] degrees_east
                : height(1) = [2] m
Auxiliary coords:
>>> f.unsqueeze()
>>> print f
Data            : air_temperature(height, time, latitude, longitude)
Cell methods    : time: mean
Dimensions      : time(1) = [15] days since 1860-1-1
                : latitude(73) = [-90, ..., 90] degrees_north
                : longitude(96) = [0, ..., 356.25] degrees_east
                : height(1) = [2] m
Auxiliary coords:

'''
        space           = self.space
        dimensions      = space.dimensions
        data_dimensions = dimensions['data']

        # --------------------------------------------------------
        # Unsqueeze everything
        # --------------------------------------------------------
        # Expand any the data
        for dim, size in space.dimension_sizes.iteritems():
            if size == 1 and dim not in data_dimensions:
                self.expand_dims(dim)
        #--- End: for

        return
    #--- End: def

#--- End: class


# ====================================================================
#
# SubspaceField object
#
# ====================================================================

class SubspaceField(SubspaceVariable):
    '''

'''
    __slots__ = []

    def __getitem__(self, indices):
        '''
x.__getitem__(indices) <==> x[indices]

'''
        field = self.variable

        # Parse the index
        indices = parse_indices(field, indices)
        
        # Initialise the output field
        new = field.copy(_omit_Data=True)

        ## Work out if the indices are equivalent to Ellipsis and
        ## return if they are.
        #ellipsis = True
        #for index, size in zip(indices, field.shape):
        #    if index.step != 1 or index.stop-index.start != size:
        #        ellipsis = False
        #        break
        ##--- End: for
        #if ellipsis:
        #    return new

        # ------------------------------------------------------------
        # Subspace the field's data
        # ------------------------------------------------------------
        new.Data = field.Data[tuple(indices)]

        # ------------------------------------------------------------
        # Subspace ancillary variables.
        # 
        # If this is not possible for a particular ancillary variable
        # then it will be discarded from the output field.
        # ------------------------------------------------------------
        s = None
        if hasattr(field, 'ancillary_variables'):
            new.ancillary_variables = AncillaryVariables()

#            s = field.space.analyse()

            for anc in field.ancillary_variables:
                dim_map = anc.space.map_dims(field.space)
                anc_indices = []
                flip_dims = []

                for adim in anc.space.dimensions['data']:
                    if anc.space.dimension_sizes[adim] == 1:
                        # Size 1 dimensions are always ok
                        anc_indices.append(slice(None))
                        continue

                    if adim not in dim_map:
                        # Unmatched size > 1 dimensions are not ok
                        anc_indices = None
                        break

                    dim = dim_map[adim]
                    if dim in field.space.dimensions['data']:
                        # Matched dimensions spanning the data arrays
                        # are ok
                        i = field.space.dimensions['data'].index(dim)
                        anc_indices.append(indices[i])
                        if anc.Data.direction[adim] != new.Data.direction[dim]:
                            flip_dims.append(adim)
                    else:
                        anc_indices = None
                        break                      
                #--- End: for

                if anc_indices is not None:
                    # We have successfully matched up each dimension
                    # of the ancillary variable's data array with a
                    # unique dimension in the parent field's data
                    # array, so we can keep a subspace of this
                    # ancillary field      
                    if flip_dims:
                        anc = anc.copy()
                        anc.flip(flip_dims)
                    #--- End: if   
                    new.ancillary_variables.append(anc.subspace[tuple(anc_indices)])
            #--- End: for

            if not new.ancillary_variables:
                del new.ancillary_variables
        #--- End: if

        # Return now if the field doesn't have a space
        if not hasattr(new, 'space'):
            return new

        # -----------------------------------------------------------
        # Subspace the space
        # -----------------------------------------------------------
        space = new.space
        for i, dim in enumerate(space.dimensions['data'][:]):
            # Continue if no slicing to be done on this dimension
            if indices[i] == slice(None, None, None): # dch ??
                continue

            for key in space:

                if dim not in space.dimensions[key]:
                    # This space component does not span the
                    # current dimension so continue
                    continue

                dice       = [slice(None)] * space[key].ndim
                pos        = space.dimensions[key].index(dim)
                dice[pos]  = indices[i]
                space[key] = space[key].subspace[tuple(dice)]

                # Update the space's dimension size
                space.dimension_sizes[dim] = space[key].shape[pos]
            #--- End: for
        #--- End: for

        if space.transforms:
            if s is None:
                s = space.analyse()        
                
            broken = []

            for key, transform in space.transforms.iteritems():
                if transform.isgrid_mapping:
                    continue

                # Still here? Then try to subspace a formula_terms
                # transform.
                for term, variable in transform.iteritems():
                    if not isinstance(variable, Field):
                        continue

                    # Still here? Then try to subspace a formula_terms
                    # field.
                    dim_map = variable.space.map_dims(space)
                    v_indices = []
                    flip_dims = []

                    for vdim in variable.space.dimensions['data']:
                        if variable.space.dimension_sizes[vdim] == 1:
                            # We can always index a size 1 dimension
                            # of the data array
                            v_indices.append(slice(None))
                            continue
                        
                        if vdim not in dim_map:
                            # Unmatched size > 1 dimensions are not ok
                            v_indices = None
                            break

                        dim = dim_map[vdim]
                        if dim in space.dimensions['data']:
                            # We can index a matched dimension which
                            # spans the data array
                            i = space.dimensions['data'].index(dim)
                            v_indices.append(indices[i])
                            if variable.Data.direction[vdim] != new.Data.direction[dim]:
                                flip_dims.append(vdim)
                        else:
                            v_indices = None
                            break                      
                    #--- End: for

                    if v_indices is not None:
                        # This formula terms variable is subspaceable
                        if flip_dims:
                            variable = variable.copy()
                            variable.flip(flip_dims)
                        #--- End: if
                        transform[term] = variable.subspace[tuple(v_indices)]
                    else:
                        # This formula terms is broken
                        broken.append(key)
                        break
                #--- End: for
            #--- End: for
                
            # Get rid of broken formula terms
            for key in broken:
                del space.transforms[key]                
                for variable in new.space.itervalues():
                    if key in getattr(variable, 'transforms', []):
                        variable.transforms.remove(key)
            #--- End: for

            for variable in new.space.itervalues():
                if hasattr(variable, 'transforms') and not variable.transforms:
                    del variable.transforms
            #--- End: for
        #--- End: if

        return new
    #--- End: def

    def __call__(self, **kwargs):
        '''

Return a subspace of the field defined by coordinate values.

:Parameters:

    kwargs : optional
        Keyword names identify coordinates and keyword values specifiy
        coordinate values.

:Returns:

    out : tuple or None

**Examples**

'''
        field = self.variable

        if not kwargs:
            return field.copy()

        indices = field.indices(**kwargs)

        return field.subspace[indices]
    #--- End: def

#--- End: class


def _conform_for_assignment(ref, other, method):
    '''

Conform `other` so that it is ready for metadata-unaware assignment
broadcasting across `ref`.

Called by the setitem and setmask methods.

`other` is not changed in place.

:Parameters:

    ref : Field
        The field that `other` is conformed against.

    other : Field
        The field to conform.

    method : str
        The name of the method calling this function. Used for
        meaningful error messages.

:Returns:

    out : Field
        The conformed version of `other`.

**Examples**

>>> new = _conform_for_assignment(f, value, 'setitem')

'''
    # Analyse each space
    s = ref.space.analyse()
    v = other.space.analyse()

    if s['warnings'] or v['warnings']:
        raise ValueError("Can't setitem: %s" % (s['warnings'] or v['warnings']))

    # Find the set of matching dimensions
    matching_ids = set(s['id_to_dim']).intersection(v['id_to_dim'])
    if not matching_ids:
         raise ValueError("Can't %s: No matching dimensions" % method)

    # ------------------------------------------------------------
    # Check that any matching dimensions defined by auxiliary
    # coordinates are done so in both fields.
    # ------------------------------------------------------------
    for identity in matching_ids:
        if (identity in s['id_to_aux']) + (identity in v['id_to_aux']) == 1:
            raise ValueError(
"Can't %s: '%s' dimension defined by auxiliary in only 1 field" %
(method, identity))
    #--- End: for

    # Copy other, as we could be about to change it.
    other = other.copy()

    # ------------------------------------------------------------
    # Check that all undefined dimensions in value have size 1,
    # and squeeze any such dimensions.
    # ------------------------------------------------------------
    for dim1 in v['undefined_dims']:
        if other.space.dimension_sizes[dim1] == 1:
            other.squeeze(dim1, from_space=True)
        else:
            raise ValueError(
                "Can't %s: Can't broadcast size %d undefined dimension" %
                (method, other.space.dimension_sizes[dim1]))
    #--- End: for

    # ------------------------------------------------------------
    # Check that all of value's unmatched but defined dimensions
    # have size 1, and squeeze any such dimensions.
    # ------------------------------------------------------------
    for identity in set(v['id_to_dim']).difference(matching_ids):
        dim1 = v['id_to_dim'][identity]
        if other.space.dimension_sizes[dim1] == 1:
            other.squeeze(dim1, from_space=True)
        else:
            raise ValueError(
                "Can't %s: Can't broadcast size %d '%s' dimension" %
                (method, other.space.dimension_sizes[dim1], identity))
    #--- End: for

    # ------------------------------------------------------------
    # Permute the axes of other.Data so that they are in the same
    # orer as their matching counterparts in ref.Data
    #
    # For example, if   ref.Data is        P T Z Y X A
    #              and  other.Data is            Y X T
    #              then other.Data becomes       T Y X
    # ------------------------------------------------------------
    axes = []       
    for dim0 in ref.space.dimensions['data']:            
        identity = s['dim_to_id'][dim0]
        if identity in matching_ids:
            dim1 = v['id_to_dim'][identity]                
            if dim1 in other.space.dimensions['data']:
                axes.append(dim1)
    #--- End: for
    other.transpose(axes)

    # ------------------------------------------------------------
    # Insert size 1 dimensions into other.Data to match dimensions
    # in ref.Data which other.Data doesn't have.
    #
    # For example, if   ref.Data is        P T Z Y X A
    #              and  other.Data is            T Y X
    #              then other.Data becomes 1 T 1 Y X 1
    # ------------------------------------------------------------
    for i, dim0 in enumerate(ref.space.dimensions['data']):
        identity = s['dim_to_id'][dim0]
        if identity in matching_ids:
            dim1 = v['id_to_dim'][identity]
            if dim1 not in other.space.dimensions['data']:
                other.expand_dims(axis=i)
        else:
             other.expand_dims(axis=i)
    #--- End: for

    # ----------------------------------------------------------------
    # Make sure that matching dimensions have the same directions
    # ----------------------------------------------------------------
    for identity in matching_ids:
        dim1 = v['id_to_dim'][identity]
        if other.space.dimension_sizes[dim1] > 1:
            dim0 = s['id_to_dim'][identity]
            if other.Data.direction[dim1] != ref.Data.direction[dim0]:
                other.flip(dim1)
    #--- End: for

    return other
#--- End: def
